package fr.inserm.u946.plotter;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Vector;

import javax.imageio.ImageIO;
import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JDesktopPane;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JInternalFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;
import javax.swing.event.InternalFrameAdapter;
import javax.swing.event.InternalFrameEvent;
import javax.swing.table.AbstractTableModel;

import fr.cephb.io.IOUtils;
import fr.cephb.util.Cast;


@SuppressWarnings("serial")
public class Plotter extends JFrame
	{
	private static final long serialVersionUID = 1L;
	private static final int GRAPH_HEIGHT=300;
	private static final int FEATURE_HEIGHT=12;

	/**
	 * SQLSource
	 *
	 */
	@SuppressWarnings("unused")
	private static class SQLSource
		{
		String uri;
		String name;
		@Override
		public String toString() {
			return name;
			}
		}
	
	
	/**
	 * 
	 * KnownGene
	 *
	 */
	@SuppressWarnings("unused")
	public static class KnownGene
		{                                  	
			/** Name of gene */
		
		private String _name = "" ;
			/** Reference sequence chromosome or scaffold */
		private String _chrom = "" ;
			/** + or - for strand */
		private char _strand = ' ' ;
			/** Transcription start position */
		private int _txStart = 0 ;
			/** Transcription end position */
		private int _txEnd = 0 ;
			/** Coding region start */
		private int _cdsStart = 0 ;
			/** Coding region end */
		private int _cdsEnd = 0 ;
			/** Number of exons */
		private int _exonCount = 0 ;
			/** Exon start positions */
		private String _exonStarts = "" ;
			/** Exon end positions */
		private String _exonEnds = "" ;
			/** UniProt display ID for Known Genes,  UniProt accession or RefSeq protein ID for UCSC Genes */
		private String _proteinID = "" ;
			/** Unique identifier for each (known gene, alignment position) pair */
		private String _alignID = "" ;
		
	    /**
		 * Hg18KnownGene
		 * constructor from sql
		 * @param row the current sql row
		 */
		public KnownGene(java.sql.ResultSet row) throws java.sql.SQLException
			{
			this._name= row.getString("name");
			this._chrom= row.getString("chrom");
			this._strand= row.getString("strand").charAt(0);
			this._txStart= row.getInt("txStart");
			this._txEnd= row.getInt("txEnd");
			this._cdsStart= row.getInt("cdsStart");
			this._cdsEnd= row.getInt("cdsEnd");
			this._exonCount= row.getInt("exonCount");
			
			try
				{
				this._exonStarts = IOUtils.getReaderContent(row.getCharacterStream("exonStarts"));
				}
			catch(java.io.IOException _err)
				{
				throw new java.sql.SQLException(_err);
				}
	
			try
				{
				this._exonEnds =IOUtils.getReaderContent(row.getCharacterStream("exonEnds"));
				}
			catch(java.io.IOException _err)
				{
				throw new java.sql.SQLException(_err);
				}
	
			this._proteinID= row.getString("proteinID");
			this._alignID= row.getString("alignID");
			}	
		}

	
	/**
	 * 
	 * ColumnDef
	 *
	 */
	private static class ColumnDef
		{
		int index=0;
		String name;
		
		public ColumnDef(String namen,int index)
			{
			this.name=namen;
			this.index=index;
			}
		
		boolean is_double=true;
		@Override
		public String toString() {
			return name;
			}
		}
	
	/**
	 * 
	 * Row
	 *
	 */
	private static class Row
		{
		private String tokens[];
		int start=0;
		int end=0;
		
		public Row(String tokens[])
			{
			this.tokens=tokens;
			}
		
		public int size()
			{
			return this.tokens.length;
			}
		
		public String get(int i)
			{
			return i< size() ?tokens[i]:"N/A";
			}
		}
	
	
	private static class SpreadSheet
		extends AbstractTableModel
		{
		private File file;
		private List<ColumnDef> headers=new ArrayList<ColumnDef>();
		private List<Row> rows= new ArrayList<Row>();
		
		public Row get(int i)
			{
			return this.rows.get(i);
			}
		@Override
		public int getColumnCount() {
			return headers.size();
			}
		@Override
		public int getRowCount() {
			return this.rows.size();
			}
		
		@Override
		public boolean isCellEditable(int rowIndex, int columnIndex) {
			return false;
			}
		
		@Override
		public String getColumnName(int column) {
			return headers.get(column).name;
			}
		
		@Override
		public Class<?> getColumnClass(int columnIndex) {
			return String.class;
			}
		
		@Override
		public Object getValueAt(int rowIndex, int columnIndex) {
			return get(rowIndex).get(columnIndex);
			}
		}
	
	
	
	
	private class DisplayPane extends JPanel
		implements ActionListener
		{
		int selected_index=-1;
		String chrom;
		List<Row> rows=new ArrayList<Row>();
		List<KnownGene> genes=new ArrayList<KnownGene>();
		int chromStart;
		int chromLength;
		int rs_col=-1;
		int viewStart;
		int viewLength;
		private JPanel drawingArea;
		private double min_y;
		private double max_y;
		JLabel infoLoc;
		private int mouse_start=-1;
		private int mouse_end=-1;
		
		DisplayPane()
			{
			super(new BorderLayout());
			this.setBorder(new EmptyBorder(3, 3, 3, 3));
			this.drawingArea=new JPanel(null)
				{
				protected void paintComponent(Graphics g)
					{
					super.paintComponent(g);
					paintDrawingArea(Graphics2D.class.cast(g));
					};
				};
			this.drawingArea.setOpaque(true);
			this.drawingArea.setBackground(Color.WHITE);
			this.add(new JScrollPane(this.drawingArea),BorderLayout.CENTER);
			
			JPanel top=new JPanel(new FlowLayout());
			this.add(top,BorderLayout.NORTH);
			
			AbstractAction action=new AbstractAction("Zoom in")
				{
				@Override
				public void actionPerformed(ActionEvent arg0) {
					if(viewLength<=1) return;
					int mid= viewStart+viewLength/2;
					viewLength=(int)(viewLength*0.9);
					if(viewLength<=0) viewLength=1;
					viewStart=Math.max(chromStart,mid-viewLength);
					reloadData();
					}
				};
			top.add(new JButton(action));
			
			action=new AbstractAction("Zoom out")
				{
				@Override
				public void actionPerformed(ActionEvent arg0) {
					int mid=viewStart+viewLength/2;
					viewLength+=(int)viewLength*0.1;
					if(viewLength<=0) viewLength=1;
					viewStart= Math.max(mid-viewLength,chromStart);
					if(viewLength+viewStart> chromStart+chromLength)
						{
						viewLength= (chromStart+chromLength-viewStart);
						}
					reloadData();
					}
				};
			top.add(new JButton(action));
			
			action=new AbstractAction("<")
				{
				@Override
				public void actionPerformed(ActionEvent arg0) {
					int shift=viewLength/10;
					viewStart= Math.max(viewStart-shift,chromStart);
					reloadData();
					}
				};
			top.add(new JButton(action));
			
			action=new AbstractAction(">")
				{
				@Override
				public void actionPerformed(ActionEvent arg0) {
					int shift=viewLength/10;
					viewStart= Math.min(viewStart+shift,chromStart+chromLength-viewLength);
					
					reloadData();
					}
				};
			top.add(new JButton(action));
			
			action=new AbstractAction("Save As....")
				{
				@Override
				public void actionPerformed(ActionEvent arg0) {
					JFileChooser fc= new JFileChooser();
					if(fc.showSaveDialog(DisplayPane.this)!=JFileChooser.APPROVE_OPTION) return;
					File f=fc.getSelectedFile();
					if(f!=null && f.exists())
						{
						if(JOptionPane.showConfirmDialog(DisplayPane.this, "Overwitre ?","Warning",JOptionPane.OK_CANCEL_OPTION,JOptionPane.WARNING_MESSAGE,null)!=JOptionPane.OK_OPTION)
							{
							return;
							}
						}
					try {
						Dimension d= new Dimension(drawingArea.getPreferredSize());
						BufferedImage img= new BufferedImage(d.width, d.height, BufferedImage.TYPE_INT_RGB);
						Graphics2D g=img.createGraphics();
						g.setColor(Color.WHITE);
						g.fillRect(0, 0, d.width, d.height);
						paintDrawingArea(g);
						g.dispose();
						ImageIO.write(img, "jpeg", f);
						} 
					catch (Exception e)
						{
						JOptionPane.showMessageDialog(DisplayPane.this, e.getMessage());
						}
					}
				};
				top.add(new JButton(action));
			
			top.add(this.infoLoc=new JLabel(""));
			
			MouseAdapter mouse= new MouseAdapter()
				{
				@Override
				public void mousePressed(MouseEvent e)
					{
					mouse_start=-1;
					mouse_end=-1;
					if(e.getY()>= GRAPH_HEIGHT) return;
					mouse_start=e.getX();
					}
				@Override
				public void mouseDragged(MouseEvent e) {
					if(mouse_start!=-1)
						{
						Graphics2D g= (Graphics2D)drawingArea.getGraphics();
						g.setXORMode(drawingArea.getBackground());
						if(mouse_end!=-1)
							{
							g.fillRect(
								Math.min(mouse_start, mouse_end),
								0,
								Math.abs(mouse_start - mouse_end),
								GRAPH_HEIGHT
								);
							}
						mouse_end=e.getX();
						g.fillRect(
								Math.min(mouse_start, mouse_end),
								0,
								Math.abs(mouse_start - mouse_end),
								GRAPH_HEIGHT
								);
						g.dispose();
						}
					}
				@Override
				public void mouseReleased(MouseEvent e) {
					if(mouse_start!=-1 && mouse_end!=-1)
						{
						viewStart = pixel2pos(Math.min(mouse_start, mouse_end));
						viewLength = Math.abs(pixel2pos(mouse_start)-pixel2pos(mouse_end));
						if(viewStart< chromStart) viewStart=chromStart;
						if(viewStart+viewLength> chromStart+chromLength)
							{
							viewLength=(chromStart+chromLength)-viewStart;
							}
						if(viewLength<1) viewLength=1;
						
						reloadData();
						drawingArea.repaint();
						}
					}
				};
			this.drawingArea.addMouseListener(mouse);
			this.drawingArea.addMouseMotionListener(mouse);
			}
		
		int pixel2pos(int i)
			{
			return viewStart+(int)((i/(double)drawingArea.getWidth())*this.viewLength);
			}
		
		double pos2pixel(int i)
			{
			return ((i-viewStart)/((viewLength)*1.0))*this.drawingArea.getWidth();
			}
		
		
		
		private void paintDrawingArea(Graphics2D g)
			{
			final int graph_height=GRAPH_HEIGHT;
			final int feature_height=FEATURE_HEIGHT;
			g.setColor(Color.LIGHT_GRAY);
			g.fillRect(0,0, drawingArea.getWidth(), GRAPH_HEIGHT);
		
			if(this.min_y!=this.max_y && selected_index!=-1)
				{
				for(Row row:this.rows)
					{
					if(row.end< this.viewStart) continue;
					if(row.start>  this.viewStart+this.viewLength) continue;
					
					int x1=(int)pos2pixel(row.start);
					int x2=(int)pos2pixel(row.end);
					double v= Double.parseDouble(row.get(this.selected_index));
					double y1=(graph_height-FEATURE_HEIGHT)-((v-this.min_y)/(this.max_y-this.min_y))*(graph_height-FEATURE_HEIGHT);
					
					g.setColor(Color.LIGHT_GRAY);
					g.drawLine((x1+x2)/2, graph_height, (x1+x2)/2, drawingArea.getHeight());
					
					g.setColor(Color.RED);
					g.fillRect(x1, (int)y1, Math.max(1,x2-x1), feature_height);
					g.setColor(Color.BLACK);
					g.drawRect(x1, (int)y1, Math.max(1,x2-x1), feature_height);
					g.setColor(Color.DARK_GRAY);
					g.drawString(row.get(this.rs_col), x2+1, (int)y1+ feature_height);
					}
				int y=graph_height;
				
				for(KnownGene gene:genes)
					{
					
					if(gene._txEnd< this.viewStart) continue;
					if(gene._txStart>  this.viewStart+this.viewLength) continue;
					y+=2;
					int x1=(int)pos2pixel(gene._txStart);
					int x2=(int)pos2pixel(gene._txEnd);
					
					
					//System.err.println(d.shape+" "+x1+"/"+x2+ "="+ g._txStart+"<"+g._txEnd+" :"+chromStart+"< "+chromEnd);
					g.setColor(Color.GREEN);
					g.fillRect(x1,y,(x2-x1),feature_height);
					g.setColor(Color.BLACK);
					g.drawRect(x1,y,(x2-x1),feature_height);
					g.setColor(Color.DARK_GRAY);
					g.drawString(gene._proteinID, x2+1, (int)y+ feature_height);
					y+=feature_height;
					}
				}
			
			
			
			
			g.setColor(Color.BLACK);
			g.drawLine(1, 0, 1, graph_height);
			g.drawLine(1, graph_height,drawingArea.getWidth(),graph_height);
			}
		
		void reloadData()
			{
			int y=GRAPH_HEIGHT;
			this.min_y= Double.MAX_VALUE;
			this.max_y=-Double.MAX_VALUE;
			
			if(this.selected_index!=-1)
				{
				
				for(Row row:this.rows)
					{
					double v= Double.parseDouble(row.get(this.selected_index));
					this.min_y= Math.min(this.min_y, v);
					this.max_y= Math.max(this.max_y, v);
					}
				for(KnownGene gene:genes)
					{
					if(gene._txEnd< this.viewStart) continue;
					if(gene._txStart>  this.viewStart+this.viewLength) continue;
					y+=2+FEATURE_HEIGHT;
					}
				
				
				}
			Dimension d= new Dimension(drawingArea.getWidth(),y);
			drawingArea.setPreferredSize(d);
			drawingArea.setMinimumSize(d);
			drawingArea.validate();
			this.validate();
			this.drawingArea.repaint();
			infoLoc.setText(String.valueOf(this.viewStart)+" - "+(this.viewStart+this.viewLength));
			}
		
		@Override
		public void actionPerformed(ActionEvent e)
			{
			selected_index=-1;
			JComboBox box=(JComboBox)e.getSource();
			ColumnDef def=(ColumnDef)box.getSelectedItem();
			if(def==null) return;
			selected_index=def.index;
			reloadData();
			}
		}
	
	private class IFrame extends JInternalFrame
		{
		//private SpreadSheet sheet;
		
		public IFrame(SpreadSheet sheet)
			{
			super(sheet.file.getName(),true,true,true,true);
			//this.sheet=sheet;
			this.addInternalFrameListener(new InternalFrameAdapter()
				{
				@Override
				public void internalFrameClosing(InternalFrameEvent e)
					{
					IFrame.this.setVisible(false);
					IFrame.this.dispose();
					}
				});
			
			JPanel content= new JPanel(new BorderLayout(5,5));
			content.setBorder(new EmptyBorder(5,5,5,5));
			JPanel top=new JPanel(new FlowLayout(FlowLayout.LEADING));
			content.add(top,BorderLayout.NORTH);
			Vector<ColumnDef> columns=new Vector<ColumnDef>();
			for(ColumnDef def:sheet.headers)
				{
				if(def.is_double) columns.add(def);
				}
			JComboBox combo=new JComboBox(columns);
			top.add(new JLabel("Column:"));
			top.add(combo);
			
			setContentPane(content);
			JTabbedPane tab= new JTabbedPane();
			content.add(tab,BorderLayout.CENTER);
			tab.addTab("Data",new JScrollPane(new JTable(sheet)));
			
			int rs_col=-1;
			for(int i=0;i< sheet.getColumnCount();++i)
				{
				if(sheet.getColumnName(i).equalsIgnoreCase("rs"))
					{
					rs_col=i;
					break;
					}
				}
			
			Map<String,DisplayPane> chrom2display=new HashMap<String,DisplayPane>();
			
			if(rs_col==-1) return;
			try
				{
				Connection con= DriverManager.getConnection(
						"jdbc:mysql://genome-mysql.cse.ucsc.edu/hg19",
						"genome",
						""
						);
				PreparedStatement pstmt= con.prepareStatement(
						"select chrom,chromStart,chromEnd from snp130 where name=?");
				for(Row row:sheet.rows)
					{
					pstmt.setString(1, row.get(rs_col));
					System.err.println(row.get(rs_col));
					ResultSet rez=pstmt.executeQuery();
					while(rez.next())
						{
						String chrom= rez.getString(1);
						DisplayPane display=chrom2display.get(chrom);
						if(display==null)
							{
							display=new DisplayPane();
							combo.addActionListener(display);
							display.chrom=chrom.toLowerCase();
							if(!display.chrom.startsWith("chr"))
								{
								display.chrom="chr"+display.chrom;
								}
							display.rs_col= rs_col;
							chrom2display.put(chrom, display);
							}
						Row row2=new Row(row.tokens);
						row2.start=rez.getInt(2);
						row2.end=rez.getInt(3);
						display.rows.add(row2);
						}
					rez.close();
					}
				
				String sql= "select * from knownGene where chrom=? and NOT(txEnd<? or txStart>?)";
				 pstmt= con.prepareStatement(
						 sql
						 );
				
				
				 
				for(DisplayPane dp: chrom2display.values())
					{
					pstmt.setString(1, dp.chrom);
					Collections.sort(dp.rows,new Comparator<Row>()
							{
							@Override
							public int compare(Row o1, Row o2) {
								int i=o1.start-o2.start;
								if(i!=0) return i;
								return o1.end-o2.end;
								}
							});
					
					dp.chromStart=dp.rows.get(0).start;
					dp.chromLength=dp.rows.get(dp.rows.size()-1).end - dp.chromStart;
					
					dp.viewStart=dp.chromStart;
					dp.viewLength=dp.chromLength;
					
					pstmt.setInt(2, dp.chromStart);
					pstmt.setInt(3, dp.chromStart+dp.chromLength);
					
					
					ResultSet rez=pstmt.executeQuery();
					while(rez.next())
						{
						KnownGene gene=new KnownGene(rez);
						dp.genes.add(gene);
						}
					rez.close();
					dp.reloadData();
					
					tab.addTab(dp.chrom,dp);
					}
				
				con.close();
				}
			catch(Exception err)
				{
				err.printStackTrace();
				JOptionPane.showMessageDialog(Plotter.this, err.getClass().getName()+":"+err.getMessage());
				}
			}
		}
	
	
	private JDesktopPane desktopPane;
	
	private Plotter()
		{
		super("Plot");
		this.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
		this.addWindowListener(new WindowAdapter() {
			@Override
			public void windowClosing(WindowEvent e)
				{
				doMenuClose();
				}
			});
		
		JPanel main=new JPanel(new BorderLayout());
		setContentPane(main);
		main.add(desktopPane=new JDesktopPane());
		
		JMenuBar bar=new JMenuBar();
		setJMenuBar(bar);
		JMenu menu=new JMenu("File");
		bar.add(menu);
		AbstractAction action=new AbstractAction("Quit")
			{
			@Override
			public void actionPerformed(ActionEvent ar) {
				doMenuClose();
				}
			};
		menu.add(action);
		action=new AbstractAction("Open...")
			{
			@Override
			public void actionPerformed(ActionEvent ar) {
				doMenuOpen();
				}
			};
		menu.add(action);
		}
	
	private void doMenuClose()
		{
		this.setVisible(false);
		this.dispose();
		}
	private void doMenuOpen()
		{
		JFileChooser chooser= new JFileChooser();
		if(chooser.showOpenDialog(this)!=JFileChooser.APPROVE_OPTION) return;
		File f= chooser.getSelectedFile();
		if(f==null ) return;
		try
			{
			SpreadSheet sheet=new SpreadSheet();
			sheet.file=f;
			String line=null;
			
			BufferedReader in=new BufferedReader(new FileReader(f));
			while((line=in.readLine())!=null)
				{
				if(line.trim().isEmpty()) continue;
				String tokens[]= line.split("[\t]");
				Row row= new Row(tokens);
				if(sheet.headers.isEmpty())
					{
					for(int i=0;i< row.size();++i)
						{
						sheet.headers.add(new ColumnDef(row.get(i),i));
						}
					}
				else
					{
					for(int i=0;i< sheet.headers.size();++i)
						{
						Double v= Cast.Double.cast(row.get(i));
						if(v==null)
							{
							sheet.headers.get(i).is_double=false;
							}
						}
					sheet.rows.add(row);
					}
				}
			in.close();
			if(sheet.getRowCount()==0) return;
			IFrame iframe=new IFrame(sheet);
			desktopPane.add(iframe);
			iframe.setBounds(5, 5, desktopPane.getWidth()-10, desktopPane.getHeight()-10);
			iframe.setVisible(true);
			}
		catch(Exception err)
			{
			err.printStackTrace();
			JOptionPane.showMessageDialog(null, err.getMessage());
			}
		
		}

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		try {
			Class.forName("com.mysql.jdbc.Driver");
			final Plotter app= new Plotter();
			Dimension d= Toolkit.getDefaultToolkit().getScreenSize();
			app.setBounds(100, 100, d.width-200, d.height-200);
			SwingUtilities.invokeAndWait(new Runnable() {
				@Override
				public void run()
					{
					app.setVisible(true);		
					}
			});
		} catch (Exception e)
			{
			JOptionPane.showMessageDialog(null, e.getMessage());
			}

	}

}
